﻿using System;
using Deferred.Library.Scene.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Deferred.Library.Scene.Utilities
{
    class TerrainBuilder
    {
        public Texture2D Heightmap{get;set;}
        private TerrainGenerationData generationData;
        private GraphicsDevice GraphicsDevice;

        private TerrainVertex[] vertices;
        private int[] indices;

        public TerrainGraphicsData GraphicsData {get; private set;}

        public TerrainBuilder(GraphicsDevice graphicsDevice, TerrainGenerationData generationData)
        {
            this.GraphicsDevice = graphicsDevice;
            this.generationData = generationData;
            GraphicsData = new TerrainGraphicsData();
        }

        public void Build()
        {
            BuildVertices();
            BuildIndices();
            BuildNormals();
            CalculateTextureWeights();
            CopyToBuffers();
        }

        private void BuildVertices()
        {
            // store the color of each pixel of the Heightmap into 2D array
            Color[] heightMapColors = new Color[Heightmap.Width * Heightmap.Height];
            Heightmap.GetData(heightMapColors);

            // declare the type of vertex the graphics card should expect when drawing these vertices
            GraphicsData.vertexDeclaration = new VertexDeclaration(GraphicsDevice, TerrainVertex.VertexElements);
            vertices = new TerrainVertex[Heightmap.Width * Heightmap.Height];

            // build each vertex of the terrain mesh
            for (int i = 0; i < heightMapColors.Length; i++)
            {
                // world coordinates of the current vertex
                float x = (i % Heightmap.Width) * generationData.Scale.X;
                float z = (i / Heightmap.Width) * generationData.Scale.Z;
                float y = heightMapColors[i].R / 2.0f * generationData.Scale.Y;

                vertices[i].Position = new Vector3(x, y, z);
                float maxScale = Math.Max(generationData.Scale.X, Math.Max(generationData.Scale.Y, generationData.Scale.Z));
                vertices[i].TextureCoordinates = new Vector2(x / (generationData.TextureScale * maxScale), z / (generationData.TextureScale * maxScale));
            }

            ApplySmoothing(generationData.Smoothing);
        }

        private void ApplySmoothing(int smooths)
        {
            for (int smooth = 0; smooth < smooths; smooth++)
            {
                for (int x = 0; x < Heightmap.Width; x++)
                {
                    for (int y = 0; y < Heightmap.Height; y++)
                    {
                        int i = x + y * Heightmap.Width;    // current vertex index
                        float sum = vertices[i].Position.Y; // sum of the heights of sampled vertices
                        int sampled = 1;                    // number of adjacent vertices plus the current

                        // indices of the adjacent vertices
                        int nw = i - Heightmap.Width - 1;
                        int n = i - Heightmap.Width;
                        int ne = i - Heightmap.Width + 1;
                        int e = i + 1;
                        int se = i + Heightmap.Width + 1;
                        int s = i + Heightmap.Width;
                        int sw = i + Heightmap.Width - 1;
                        int w = i - 1;

                        // if the current vertex is on an edge, there won't be 8 adjacent vertices
                        bool checkLeft = x != 0;
                        bool checkRight = x != Heightmap.Width - 1;
                        bool checkAbove = y != 0;
                        bool checkBelow = y != Heightmap.Height - 1;

                        // sum the adjacent vertices
                        if (checkAbove)
                        {
                            sum += vertices[n].Position.Y; sampled++;
                            if (checkLeft) sum += vertices[nw].Position.Y; sampled++;
                            if (checkRight) sum += vertices[ne].Position.Y; sampled++;
                        }
                        if (checkBelow)
                        {
                            sum += vertices[s].Position.Y; sampled++;
                            if (checkLeft) sum += vertices[sw].Position.Y; sampled++;
                            if (checkRight) sum += vertices[se].Position.Y; sampled++;
                        }
                        if (checkRight) sum += vertices[e].Position.Y; sampled++;
                        if (checkLeft) sum += vertices[w].Position.Y; sampled++;

                        // assign the averaged height to the current vertex
                        vertices[i].Position.Y = sum / sampled;
                    }
                }
            }
        }

        private void BuildIndices()
        {
            int maxSquares = (Heightmap.Width - 1) * (Heightmap.Height - 1); // one square = 2 triangles
            indices = new int[maxSquares * 6]; // 2 triangles = 6 indices per square

            int index = 0;
            for (int i = 0; i < maxSquares; i++)
            {
                int topleft = i % (Heightmap.Width - 1) + (i / (Heightmap.Width - 1)) * Heightmap.Width;
                int topright = topleft + 1;
                int bottomright = topright + Heightmap.Width;
                int bottomleft = topleft + Heightmap.Width;

                // upper right triangle of square
                indices[index++] = topleft;
                indices[index++] = topright;
                indices[index++] = bottomright;

                // bottom left triangle of square
                indices[index++] = topleft;
                indices[index++] = bottomright;
                indices[index++] = bottomleft;
            }
        }

        private void BuildNormals()
        {
            // initialize each vertex normal to the zero vector
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = Vector3.Zero;

            // add normals from each triangle the vertex is part of
            for (int i = 0; i < indices.Length / 3; i++)
            {
                // indices for the vertices of the current triangle
                int[] triangleIndices = new int[3];
                for (int v = 0; v < 3; v++)
                    triangleIndices[v] = indices[i * 3 + v];

                // calculate the normal & tangent vectors for the surface of the triangle
                Vector3 edge1 = vertices[triangleIndices[0]].Position - vertices[triangleIndices[2]].Position;
                Vector3 edge2 = vertices[triangleIndices[0]].Position - vertices[triangleIndices[1]].Position;
                Vector3 normal = Vector3.Normalize(Vector3.Cross(edge1, edge2));

                // assign the normal and tangent vectors to each vertex in the triangle
                for (int v = 0; v < 3; v++)
                {
                    vertices[triangleIndices[v]].Normal += normal;
                    vertices[triangleIndices[v]].Tangent = Vector3.Normalize(edge1);
                    vertices[triangleIndices[v]].Binormal = Vector3.Normalize(Vector3.Cross(edge1, normal));
                }
            }

            // average the normals for each vertex
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }

        private void CopyToBuffers()
        {
            // copy vertices to vertex buffer
            GraphicsData.vBuffer = new VertexBuffer(GraphicsDevice, vertices.Length *
                TerrainVertex.SizeInBytes, BufferUsage.WriteOnly);
            GraphicsData.vBuffer.SetData(vertices);
            GraphicsData.vertexCount = vertices.Length;

            // copy indices to index buffer
            GraphicsData.iBuffer = new IndexBuffer(GraphicsDevice, typeof(int),
                indices.Length, BufferUsage.WriteOnly);
            GraphicsData.iBuffer.SetData(indices);
            GraphicsData.triangleCount = indices.Length / 3;
        }

        private void CalculateTextureWeights()
        {
            float scaledLowMark = generationData.LowMark * generationData.Scale.Y;
            float scaledHighMark = generationData.HighMark * generationData.Scale.Y;

            for (int i = 0; i < vertices.Length; i++)
            {
                float height = vertices[i].Position.Y;
                double normalAngle;
                if (vertices[i].Normal == Vector3.Up)
                    normalAngle = 90;
                else
                    normalAngle = MathHelper.ToDegrees((float)Math.Acos(Vector3.Dot(vertices[i].Normal, new Vector3(vertices[i].Normal.X, 0, vertices[i].Normal.Z))));
                Vector4 weights = Vector4.Zero; // start all weights at 0

                if (height < scaledLowMark) // height is in the lower third
                {
                    weights.Y = height / scaledLowMark;   // percent weight 2
                    weights.X = 1 - weights.Y;  // percent weight 1
                }
                else if (height < scaledHighMark) // height is in the middle third
                {
                    weights.Z = (height - scaledLowMark) / (scaledHighMark - scaledLowMark);  // percent weight 3
                    weights.Y = 1 - weights.Z;                  // percent weight 2
                }
                else // height is in the top third
                {
                    weights.W = (height - scaledHighMark) / (255 - scaledHighMark) * (float)(90 - normalAngle) / 90.0f; // percent weight 4
                    weights.Z = 1 - weights.W;                  // percent weight 3
                }

                // surfaces which have steep surfaces should be rocky
                if (normalAngle < 50)
                {
                    float rockyness = MathHelper.Clamp((float)(90 - normalAngle) / 90.0f, 0, 1);
                    weights *= 1 - rockyness;
                    weights.W += rockyness;
                }
                // surfaces which have shallow angles mix in some of the third texture
                if (normalAngle > 75 && vertices[i].Position.Y > scaledLowMark)
                {
                    weights *= 0.4f;
                    weights.Z += 0.6f;
                }

                vertices[i].TextureWeights = weights;
            }
        }
    }
}
